package org.swift.mashup.wsdl;

import java.util.HashMap;
import java.util.Map;

import javax.xml.stream.XMLStreamException;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMNode;
import org.apache.log4j.Logger;
import org.jaxen.JaxenException;
import org.swift.commons.xml.OMUtils;
import org.swift.commons.xml.XmlUtils;
import org.w3c.dom.Node;

public class AxiomMessage implements OperationMessage<OMElement> {
	/** The logger for this class. */
	private final static Logger logger = Logger.getLogger(AxiomMessage.class);

	/** Factory used for creating OMElement instances. */
	private final OMFactory factory = OMAbstractFactory.getOMFactory();

	/**
	 * The soap body, which is cloned and reused for every setParameter
	 * invocation.
	 */
	private final OMElement soapBody;
	
	

	/**
	 * Map for looking up the namespace for a given element's (parameter's)
	 * local name.
	 */
	private final Map<String, OMNamespace> inputElementNamespaces = new HashMap<String, OMNamespace>();

	/**
	 * Should be VALID xml for a given service communication.
	 * 
	 * @param messageBody
	 * @throws XMLStreamException
	 *             if messageBody is not valid xml
	 * @throws JaxenException
	 */
	public AxiomMessage(String messageBody) throws XMLStreamException,
			JaxenException {		
		this(messageBody, SOAPUtil.getInputParamsNamespaces(messageBody));
	}

	/**
	 * Construct an axiom message with a message body sample. However, input
	 * elements that are generated in the body will be placed in the namespaces
	 * defined in the input element namespace map.
	 * 
	 * @param messageBody
	 * @param inputElementNamespaces
	 *            map every possible input element local name to the namespace
	 *            they should reside in when written to the SOAP body.
	 * @throws XMLStreamException
	 */
	public AxiomMessage(String messageBody,
			Map<String, OMNamespace> inputElementNamespaces)
			throws XMLStreamException, JaxenException {
		this.soapBody = SOAPUtil.selectBodyElement(messageBody);
		this.inputElementNamespaces.putAll(inputElementNamespaces);
	}

	/**
	 * Sets the parameters on the sample message that was provided, and returns
	 * the results. If there was a namespace map provided, or there were
	 * namespace examples in the sample message given at initialization, then
	 * all parameters will be placed into their corresponding namespaces.
	 */
	public OMElement setParameters(Map<?, ?> parameters) {
		if (parametersExist(parameters)) {
			return assignParameters(parameters);
		}
		return soapBody;
	}

	/**
	 * Generates a soap body element with the namespaced parameters and their
	 * respective values.
	 * 
	 * @param parameters
	 *            the map of key/value pairs to be placed in the soap body.
	 * @return soap body with properly namespaced element children.
	 */
	private OMElement assignParameters(Map<?, ?> parameters) {
		OMElement body;
		String parameterName = null;
		Object parameterValue = null;
		try {
			OMNamespace soapBodyNs = soapBody.getNamespace();
			if (soapBodyNs == null)
				soapBodyNs = soapBody.getDefaultNamespace();
			body = factory.createOMElement(soapBody.getLocalName(), soapBodyNs);
			logger
				.debug("Searching for and replacing/adding parameters to this document: "
						+ body);
			for (Object parName : parameters.keySet()) {
				parameterName = parName.toString();
				parameterValue = parameters.get(parameterName);
				if (XmlUtils.isXml(parameterValue.toString())) {
					processTextXML(body, parameterName, parameterValue
						.toString());
				} else {
					processDefaultInput(body, parameterName, parameterValue);
				}
			}
			return body;
		} catch (XMLStreamException xmlSE) {
			throw new RuntimeException(xmlSE);
		}
	}

	/**
	 * Helper function for determing if there are any params.
	 * 
	 * @param parameters
	 *            map of params to check
	 * @return <code>true</code> if there are parameters present in the map,
	 *         <code>false</code> otherwise.
	 */
	private boolean parametersExist(Map<?, ?> parameters) {
		return parameters != null && parameters.size() > 0;
	}

	/**
	 * Generates a parameter in which the value of the parameter is the XML
	 * content that was located in the value map. (A child element is created
	 * for this particular input parameter).
	 * 
	 * @param body
	 *            soap body to attach the parameter to.
	 * @param parameterName
	 *            name of the param being sent in the soap body.
	 * @param parameterValue
	 *            the xml value the parameter is set to.
	 * @throws XMLStreamException
	 *             if the parameterValue is not properly formatted XML.
	 */
	private void processTextXML(OMElement body, String parameterName,
			String parameterValue) throws XMLStreamException {
		OMElement parameterContents = OMUtils.stringToOMElement(parameterValue);
		parameterContents.setLocalName(parameterName);
		parameterContents.setNamespace(body.getNamespace());
		body.addChild(parameterContents);
	}

	/**
	 * This is the most commonly used method that is invoked when a given
	 * parameter's value is a simple, primitive type or list of primitive types.
	 * The input element is generated and attached to the soap body, in the
	 * appropriate namespace, if applicable.
	 * 
	 * @param body
	 *            soap body to attach the parameter to
	 * @param parameterName
	 *            name of the parameter to be placed into the soap body
	 * @param parameterValue
	 *            non-xmltext value that the parameter should be set to in the
	 *            soap body.
	 * @throws XMLStreamException
	 *             if the value is an Node and there is an issue marshalling it
	 *             to OMElement
	 */
	private void processDefaultInput(OMElement body, String parameterName,
			Object parameterValue) throws XMLStreamException {
		if (parameterValue instanceof Object[]) {
			for (Object value : (Object[]) parameterValue) {
				processDefaultInput(body, parameterName, value);
			}
			return;
		}
		Object value = null;
		String[] sa = ((String) parameterValue).split(",");
		boolean valueIsDom = false;
		for (int i = 0; i < sa.length; i++) {
			value = sa[i];
			valueIsDom = false;
			if (value == null) {
				value = "";
			}
			if (OMNode.class.isAssignableFrom(value.getClass())) {
				valueIsDom = true;
			} else if (Node.class.isAssignableFrom(value.getClass())) {
				value = OMUtils.stringToOMElement(((Node) value).toString());
				valueIsDom = true;
			}

			if (valueIsDom) {
				body.addChild((OMNode) value);
			} else {
				addParameter(body, parameterName).setText(value.toString());
			}
		}
	}
	
	public OMElement getPayload() {
		return this.soapBody;
	}

	/**
	 * Helper method that is used for any methods that need to add a parameter
	 * to the request body. You should delegate to this method when adding a new
	 * parameter to the body because it will add the parameter to the soap body
	 * in the proper namespace.
	 * 
	 * @param body
	 *            the soap body to attach the element to.
	 * @param parameterName
	 *            the name of the parameter being added to the soap body.
	 * @return the element that was generated and placed into the soap body.
	 */
	private OMElement addParameter(OMElement body, String parameterName) {
		logger.debug("Creating a new element and adding it to the request ["
				+ body + "]");
		OMElement parameter;
		OMNamespace desiredNS = inputElementNamespaces.get(parameterName);
		parameter = factory.createOMElement(parameterName, desiredNS);
		body.addChild(parameter);
		return parameter;
	}
}